/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.stars; 

import com.chasekernan.hxnova.core.components.Defense;
import com.chasekernan.hxnova.core.components.StarScanner;
import com.chasekernan.hxnova.core.designs.HullDesign;
import com.chasekernan.hxnova.core.fleets.Fleet;
import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.races.TechLevel;
import com.chasekernan.hxnova.core.stars.Star;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.utils.Utils;
import com.chasekernan.hxnova.core.dataholders.MessageHandler;

/**
    The different possible production types.
**/
enum ProductionType {
    Factories;
    Mines;
    Defenses;
    PlanetaryScanner;
    //Terraform; Not implemented yet...
    //MineralAlchemy;
    Starbase(design : HullDesign);
    Ship(design : HullDesign);
}

typedef ResAndMin = {
    var resources : Int;
    var minerals : MineralSet;
}

/**
    The production list contains these. The contributed field is the amount of
    resources/minerals that have been already put into its production.
    Note: auto is included, but not implemented yet.
**/
typedef ProductionItem = {
    var type : ProductionType;
    var amount : Int;
    var auto : Bool;
    var contributed : ResAndMin;
}

/**
    Each inhabited star system has a production queue filled with production 
    items.
**/
class ProductionQueue {
    
    /**
        Should probably be placed in the Race class.
    **/
    public static var FACTORY_MINERAL_COST = setupFM();
    public static var REDUCED_FACTORY_MINERAL_COST = setupRFM();
    
    private static function setupFM() : MineralSet {
        var m : MineralSet = new MineralSet();
        m.setValue("germanium", 4);
        return m;
    }
    private static function setupRFM() : MineralSet {
        var m : MineralSet = new MineralSet();
        m.setValue("germanium", 3);
        return m;
    }
    
    
    public var parent : Star;
    public var production : Array<ProductionItem>;
    
    /**
        Sets up an empty queue.
    **/
    public function new(parent : Star) {
        this.parent = parent;
        production = new Array();
    }
    
    /**
        Adds an item to the list at the bottom unless pos is specified.
        Note: if the pos is out of bounds, the item is simply added to the top
        or bottom of the list and no error is thrown.
    **/
    public function add(type : ProductionType, amount : Int, ?auto : Bool, 
            ?pos : Int) {
        var c = { resources : 0, minerals : new MineralSet() };
        var obj = { type : type, amount : amount, auto : (auto == true), 
                contributed : c };
        checkItem(obj);
        
        addObject(obj, pos);
    }
    
    /**
        Used mainly inside the class, but instead of specifying the item's 
        fields in the function parameters, this just takes the production item.
    **/
    public function addObject(obj : ProductionItem, ?pos : Int) {
        checkItem(obj);
        
        if (pos == null) pos = production.length;
        
        if(pos >= production.length) production.push(obj);
        else if(pos <= 0) production.insert(0, obj);
        else production.insert(pos, obj);
    }
    
    /**
        Unfortunately the Haxe std library doesn't have a function to move items 
         in an array so this is a quick implementation.
    **/
    public function move(objIndex :  Int, up : Bool) {
        if(production.length < 2) return;
        
        if((objIndex == 0 && up) || (objIndex == (production.length - 1) && 
                !up)) return;
        
        var obj = production[objIndex];
        
        if(up) {
            if(objIndex == 1) {
                production.splice(1, 1);
                production.unshift(obj);
                return;
            }
            
            var top = new Array<ProductionItem>();
            for (i in 0...(objIndex - 1)) top.push(production[i]);
            top.push(obj);
            top.push(production[objIndex - 1]);
            for (i in (objIndex + 1)...production.length) 
                top.push(production[i]);
            production = top;
            
        } else {
            if(objIndex == production.length - 2) {
                var bottomObj = production.pop();
                production.pop();
                production.push(bottomObj);
                production.push(obj);
                return;
            }
            
            move(objIndex + 1, true);
        }
    }
    
    private function checkItem(p : ProductionItem) {
        if (p.amount <= 0) throw "Cannot produce less than 1 item.";
        if (p.contributed.resources < 0) 
            throw "Tried to contribute less than 0 resources.";
        checkType(p.type, p.amount, p.auto);
    }
    
    private function checkType(t : ProductionType, amount : Int, auto : Bool) {
        switch(t) {
            case Factories:
                if (parent.factories + amount > parent.getPossibleFactories() &&
                        !auto) throw "Cannot build this many factories.";
            case Mines:
                if (parent.mines + amount > parent.getPossibleFactories() && 
                        !auto) throw "Cannot build this many mines.";
                        
            case Defenses:
                if (parent.defenses + amount > parent.getPossibleDefenses() &&
                        !auto) throw "Cannot build this many defenses.";
                        
            case PlanetaryScanner:
                if (parent.hasScanner) 
                    throw "Star already has a planetary scanner.";
                if (amount != 1) throw "You can only build 1 scanner";
                
            //case Defenses;
            case Starbase(design):
                if (amount != 1) throw "You can only build 1 starbase.";
                if (!design.isLegal()) 
                    throw "Not a legal design: " + design.name;
                if (!design.isStarbase()) 
                    throw "Tried to build a starbase off of a design that " + 
                            "doesn't produce a starbase.";
                checkDesign(design);
            case Ship(design):
                if (!design.isLegal()) 
                    throw "Not a legal design: " + design.name;
                if (design.isStarbase()) 
                    throw "Cannot produce a fleet that has a starbase design:" +
                            " " + design.name;
                checkDesign(design);
        }
    }

    private function checkDesign(design : HullDesign)  {
        if (!design.isAvailableTo(parent.owner.race)) 
            throw "Selected design is not available to this race: " + 
                    design.name;
                    
        var needed = design.getTechLevelNeeded();
        var have = parent.owner.techLevel;
        if (needed.isGreaterThanOrEqualTo(have) && !needed.isEqualTo(have))
            throw "You do not have the tech level needed to produce this " +
                    "design: " + design.name;
    }
    
    
    
    public function getCost(item : ProductionItem) : ResAndMin {
        var indivCost = switch(item.type) {
            
            case Factories:
                var res = parent.owner.race.factoryCost;
                var mins = if(parent.owner.race.factoriesCostLessMin) 
                        REDUCED_FACTORY_MINERAL_COST.clone() else 
                        FACTORY_MINERAL_COST.clone();
                {resources : res, minerals : mins };
                
            case Mines:
                {resources : parent.owner.race.mineCost, 
                        minerals : new MineralSet() };
                        
            case Defenses:
                Defense.COST;
                
            case PlanetaryScanner:
                StarScanner.COST;
            
            case Starbase(design):
                parent.owner.getCostForDesign(design);
                
            case Ship(design):
                parent.owner.getCostForDesign(design);
        }
            
        return { resources : indivCost.resources * item.amount, 
                minerals : indivCost.minerals.multiply(item.amount) };
    }
    
    private function buildItem(item : ProductionItem, resources : Int, 
            minerals : MineralSet) : Int {
        var totalCost = getCost(item);
        
        resources -= totalCost.resources;
        minerals.subtract(totalCost.minerals);
        
        switch(item.type) {
            
            case Factories:
                parent.factories += item.amount;
                Global.messages.sendMessage( { to : Target.PlayerTarget(parent.owner),
                        from : Target.Host, data : 
                        MessageData.ProducedFactories(item.amount, parent.name) 
                } );
                
            case Mines:
                parent.mines += item.amount;
                Global.messages.sendMessage( { to : Target.PlayerTarget(parent.owner), 
                        from : Target.Host, data : 
                        MessageData.ProducedMines(item.amount, parent.name) } );
                        
            case Defenses:
                parent.defenses += item.amount;
                Global.messages.sendMessage( { to : Target.PlayerTarget(parent.owner), 
                        from : Target.Host, data : 
                        MessageData.ProducedDefenses(item.amount, parent.name) } );
                
            case PlanetaryScanner:
                parent.hasScanner = true;
                Global.messages.sendMessage( { to : Target.PlayerTarget(parent.owner), 
                        from : Target.Host, data : 
                        MessageData.ProducedScanner(parent.name) } );
                        
            case Starbase(design):
                parent.starbase = StarStarbase.Design(design);
                
            case Ship(design):
                var f : Fleet = new Fleet();
                f.location = parent.location.clone();
                f.owner = parent.owner;
                f.addNewShips(design, item.amount);
                Global.fleets.appendParticle(f);
                f.waypoints = new Array();
                f.name = design.name + " #" + Std.string(f.id);
        }
            
        return resources;
    }
    
    /**
        Runs through the production queue and builds every production item using 
         the availabe resources. 
        Returns the resources that are not used in production (so that they can
        be used in research). 
     */
    public function run() : Int {
        if (parent.owner == null || 
                parent.owner.id == Global.players.nullPlayer.id) {
            throw "Cannot run a production queue if the star owner is null.";
        }
        
        //check all itmes
        for (item in production) checkItem(item);
        
        var totalResources = parent.calculateResources();
        var researchResources = Std.int(parent.owner.researchField.percent 
                                        * totalResources);
        var resources = totalResources - researchResources;
        
        //TODO: Note somewhere that mins are mined in [ProductionQueue.run].
        var mins = parent.mineMinerals().add(parent.minerals);
        
        var deletions = new Array<Int>();
        
        for (i in 0...production.length) {
            var item = production[i];
            
            var totalCost = getCost(item);
            
            //can build complete item
            if (resources >= totalCost.resources && mins.isGreaterThanOrEqualTo(
                    totalCost.minerals)) {
                resources = buildItem(item, resources, mins);
                deletions.push(i);
                continue;
            } 
            
            //if out of resources, cannot build anything, but this is not true 
            //for minerals.
            if (resources == 0) break;
            
            //build partial
            var amountByRes = item.amount * (resources / totalCost.resources);
            var amountByMin = item.amount * mins.divideByMinerals(
                    totalCost.minerals);
            
            //this includes the percent of the next one to be built.
            var amountTotal = Math.min(amountByRes, amountByMin);
            
            var amountToBuild = Math.floor(amountTotal);
            
            //save initial amount
            var a = item.amount;
            
            item.amount = amountToBuild;
            resources = buildItem(item, resources, mins);
            
            item.amount = a - amountToBuild;
            //maybe a different rounding scheme here?
            item.contributed.resources = Math.floor(totalCost.resources * 
                    (amountTotal - amountToBuild));
            item.contributed.minerals = totalCost.minerals.multiply(
                    amountTotal - amountToBuild);
            
            resources -= item.contributed.resources;
            mins.subtract(item.contributed.minerals);
            
            break;
        }
        
        var i = production.length;
        while(i > 0) {
            i--;
            for(j in deletions) {
                if(j == i) production.splice(i, 1);
            }
        }
        
        parent.minerals = mins;
        
        return resources + researchResources;
    }
    
    /**
        This function checks to see if this new production queue [now] could be 
        obtained from the prior one. Essentially, it stops the player from 
        cheating by changing the contributions to production items.
    **/
    public static function checkContributions(prior : ProductionQueue, 
                now : ProductionQueue) : Bool {
        
        var priorPQ = prior.production.copy();
        var nowPQ = now.production.copy();
        
        var i = nowPQ.length;
        while(i > 0) {
            i--;
            
            var item = nowPQ[i];
            if(item.contributed.resources == 0 && 
                    item.contributed.minerals.getMass() == 0) continue;
            
            //find the type in the prior production
            var found = false;
            for(other in priorPQ) {
                if(other.contributed.resources == item.contributed.resources &&
                        other.contributed.minerals.isEqualTo(
                        item.contributed.minerals)) {
                    found = true;
                    priorPQ.remove(other);
                    break;
                }
            }
            
            if(!found) return false;
        }
        
        return true;
    }
    
    public static var XML_TITLE = "production_queue";
    
    /**
        Writes a production queue to an xml file.
        Sample xml:
            TODO: run writeToXml and insert xml here.
    **/
    public static function writeToXml(pq : ProductionQueue) : Xml {
        var xml = Xml.parse("<" + XML_TITLE + "></" + XML_TITLE + ">")
                .firstChild();
        
        if(pq.parent != null) xml.set("parent", Std.string(pq.parent.id));
        
        if(pq.production != null) {
            for(i in 0...pq.production.length) {
                var item = pq.production[i];
                //for some reason, you can't use attributes with 
                //Xml::createElement.
                var itemXml = Xml.parse("<item></item>").firstChild();
                itemXml.set("order_id", Std.string(i));
                
                itemXml.set("type", switch(item.type) {
                    
                    case Factories: "factories";
                        
                    case Mines: "mines";
                    
                    case Defenses: "defenses";
                    
                    case PlanetaryScanner: "planetary_scanner";
                    
                    case Starbase(d):
                        itemXml.set("design", d.name);
                        "starbase";
                    case Ship(d):
                        itemXml.set("design", d.name);
                        "ship";
                });
                
                itemXml.set("amount", Std.string(item.amount));
                itemXml.set("auto", if(item.auto) "true" else "false");
                
                //contributed
                var contributed = Xml.parse("<contributed></contributed>")
                        .firstElement();
                contributed.set("resources", Std.string(
                        item.contributed.resources));
                contributed.addChild(
                        MineralSet.writeToXml(item.contributed.minerals));
                
                itemXml.addChild(contributed);
                
                xml.addChild(itemXml);
                
                //TODO: Figure out xml spacer problem
                xml.addChild(Xml.createElement("spacer"));
            }
        }
        
        return xml;
    }
    
    /**
        Reads a production queue from an xml file.
        Will throw an error if there are any mistakes (this function in 
        particular has lots of error checks).
    **/
    public static function readFromXml(xml : Xml) : ProductionQueue {
        try {
            //note, do not really need to check for nulls here as either you
            //know the entire production queue or you don't.
        
            var s : Star = null;
            if (xml.exists("parent")) {
                s = Global.stars.getStarById(Std.parseInt(xml.get("parent")));
            }
            if (s == null) {
                throw "Parent attribute is needed on production queue xml " + 
                      "elements";
            }
        
            var pq = new ProductionQueue(s);
            pq.production = new Array();
            
            for (itemXml in xml.elementsNamed("item")) {
                var amount = Std.parseInt(itemXml.get("amount"));
                if(amount < 1)
                    throw "Amount must not be less than 1.";
                var auto = itemXml.get("auto") == "true";
                var pos = Std.parseInt(itemXml.get("order_id"));
                
                var type = switch(itemXml.get("type")) {
                    
                    case "factories":
                        Factories;
                        
                    case "mines":
                        Mines;
                        
                    case "defenses":
                        Defenses;
                       
                    case "planetary_scanner":
                        PlanetaryScanner;
                        
                    case "starbase":
                        var design = s.owner.getDesignByName(
                                itemXml.get("design"));
                        Starbase(design);
                        
                    case "ship":
                        var design = s.owner.getDesignByName(
                                itemXml.get("design"));
                        Ship(design);
                        
                    default:
                        throw "No such production type: " + itemXml.get("type");
                };
                
                var contributed = Utils.getXmlElement(itemXml, "contributed");
                
                var resources = Std.parseInt(contributed.get("resources"));
                if(resources < 0) 
                        throw "Contributed resources cannot be less than 0.";
                var minerals = MineralSet.readFromXml(
                        Utils.getXmlElement(contributed, MineralSet.XML_TITLE));
                        
                pq.add(type, amount, auto, pos);
                pq.production[pos].contributed = 
                        {resources : resources, minerals : minerals};
            }
            
            return pq;
        } catch (e : Dynamic) {
            throw "Error while reading production queue xml: " +e ;
        }        
    }
}


class PQUnitTest extends haxe.unit.TestCase {
    public function testMoveObj() {
        var pq = new ProductionQueue(new Star());
        //this was used to check some of the cheating protection functions.
    }
}
        
